Skip to content

fix: assistant: clear saved state on sign-out, and strip ?sessionId= after a failed open (#1999) - #2005

Open
frano-m wants to merge 1 commit into
mainfrom
frano-m/1999-assistant-signout-and-failed-open
Open

frano-m wants to merge 1 commit into
mainfrom
frano-m/1999-assistant-signout-and-failed-open

Conversation

@frano-m

@frano-m frano-m commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Closes #1999

What changed

  • useAssistantChat derives isSaved and savedAnalysisId from auth: both read as false/null while the viewer is signed out. Any save, reply or conversation load that lands after sign-out now shows as unsaved, with no highlighted History row. Before this change, a reply or load still in flight at sign-out brought "Saved to your account" back next to the sign-in prompt. The sign-out effect that cleared these values one at a time is removed; it had nothing left to do.
  • AuthProvider.logout now calls setUser(null) before the /auth/logout POST, not after it. Every account-scoped reader (for example the History list) stops as soon as the viewer clicks sign out, and doesn't send one more request against a session the server is ending.
  • A non-retryable 4xx (403, 404, …) on a restore whose session came from ?sessionId= now strips the param. That covers both an open from History and a page loaded from a ?sessionId= link. Retryable failures (no response, 5xx, 408, 429) keep it. The strip reads the router through a ref, so it builds on the current URL rather than the one from when the callback was created.

Why

Issue #1999 lists three edge cases in useAssistantChat. Review found that two of them could not happen as the issue described: the History list, the only reader of savedAnalysisId and lastSave, unmounts as soon as isAuthenticated goes false. Review also found two real gaps nearby:

  • The account-scoped 401 can happen during logout's POST, before the user is cleared.
  • The "Saved" label can come back after sign-out.

This PR fixes those at their source instead of guarding each request inside the hook. The ?sessionId= case was a real bug: every reload showed "That conversation is no longer available" until the user started a new analysis.

Assumptions I made

  • Signing in is always a full-page redirect, as the hook's existing comments state. So a signed-out viewer never becomes signed in again under the same hook state, and derived values are enough.
  • isSignedOut only applies when login is configured and auth has finished loading. While auth is loading, the label shows what the server reported, as it did before.
  • A restore marked isNamed always got its session from ?sessionId=, either on page load or because an open wrote it there. A restore from the localStorage pointer leaves the URL alone.
  • lastSave itself is not gated. Its only reader unmounts on sign-out, and the logout ordering closes the window where it could still trigger a request.

How to verify

Run the site locally with the backend and login enabled.

Definition of done from the issue:

  • Signing out clears savedAnalysisId. Sign in, open a saved conversation from History, then sign out from the account menu. History is replaced by the sign-in prompt, the panel no longer says "Saved to your account", and when you sign back in no stale row is selected.
  • A sign-in save that resolves after sign-out doesn't call setLastSave. In DevTools, throttle the network to "Slow 3G". Sign in on a conversation with unsaved messages, then sign out while the save request is pending. In the Network tab, no saved-analyses request is sent after the /auth/logout POST, and the panel does not show "Saved to your account".
  • A non-retryable 4xx on a restore started by an open strips ?sessionId=; retryable failures leave it alone. Open a saved conversation whose session the server refuses. One way: in DevTools, block the /assistant/restore request URL, or use a session id that no longer exists. The URL loses ?sessionId=, and a reload shows an empty assistant with no "no longer available" error. Also load /assistant?sessionId=<missing id> directly: you see the error once, the param is gone, and a reload is clean.
  • Each case has a regression test.
    • tests/hooks/useAssistantChat.autosave.test.ts: "signing out leaves no saved analysis showing as open", "a conversation that loads after signing out does not show as saved", "a reply that lands after signing out does not show as saved".
    • tests/hooks/useAssistantChat.restore.test.ts: "a 404 on a session named in the URL drops it from the URL", "a conversation refused after an open is dropped from the URL", "a conversation that fails to load after an open for a transient reason stays in the URL", plus a no-strip assertion for a stored pointer.
    • tests/providers/authenticationProvider.test.tsx: "signs out on the page before the server has ended the session".
    • Run npx jest tests/hooks/useAssistantChat tests/providers. The regression tests fail on main.

Note: bucketLabel in tests/views/queueStatsView.test.tsx fails locally on main too. It is unrelated to this change.

🤖 Generated with Claude Code
🐦 Built with cc-claude-tools

…` after a failed open (#1999)

`useAssistantChat` now derives `isSaved` and `savedAnalysisId` from auth, so
a save, reply or restore that lands after sign-out can't put either back.
`AuthProvider.logout` signs out on the page before the logout request, so no
account-scoped request races the session being ended. A non-retryable 4xx on
a restore whose session came from `?sessionId=` strips the param, so a reload
no longer fails the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cc-claude-tools <cc-claude-tools@clevercanary.com>
@github-actions github-actions Bot added the fix label Nov 13, 2026
@frano-m
frano-m requested a balanced review from Copilot November 13, 2026 06:37
@frano-m frano-m moved this to Ready for Review in BRC development tasks Nov 13, 2026
@frano-m
frano-m marked this pull request as ready for review November 13, 2026 06:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

There are no active review issues for the sign-out state and session-restore URL cleanup changes.

No open issues 🎉

What changed in this PR

Addresses #1999 by preventing stale saved-conversation indicators after sign-out and repeated failed restores from assistant session URLs.

Changes:

  • Derives saved-state indicators from authentication and clears the local user before the logout request completes.
  • Removes ?sessionId= after non-retryable restore failures using the latest router state, while preserving it for retryable failures.
  • Adds regression tests for sign-out timing, late responses, and restore URL cleanup.
File Description
tests/​providers/​authenticationProvider.test.tsx Verifies local sign-out occurs before the logout request completes.
tests/​hooks/​useAssistantChat.restore.test.ts Tests URL cleanup for permanent failures and preservation for transient failures.
tests/​hooks/​useAssistantChat.autosave.test.ts Tests saved-state clearing on sign-out and after late restore or reply responses.
packages/​shared/​views/​AssistantView/​hooks/​UseAssistantChat/​types.ts Documents signed-out behavior for saved-state fields.
packages/​shared/​views/​AssistantView/​hooks/​UseAssistantChat/​hook.ts Gates saved-state indicators on authentication and clears failed session URLs.
packages/​shared/​providers/​authentication/​provider.tsx Clears the local user before sending the logout request.

Review effort: Balanced

@NoopDog NoopDog left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, deriving isSaved from auth on each render is cleaner than the old sign-out effect, and moving setUser(null) ahead of the logout POST closes the real gap. The URL strip and its retryable-failure test look right too. Two required points, both about making the code and #1999 say the same thing:

  1. Required: the savedAnalysisId gate and its test protect a value nothing reads. See the inline comment at hook.ts:650.
  2. Required: make what shipped match #1999, or update #1999. Two places differ:
    • #1999's second done item says a save that resolves after sign-out must not call setLastSave. hook.ts:364 still calls it. The PR's reasoning holds: the History list is the only reader, it isn't rendered while signed out (chatHistorySelector.tsx:32), and signing in is a full-page redirect. So either gate setLastSave as the issue asks, or edit #1999's done item to record that the logout reorder covers it instead.
    • "How to verify" step 3, in both the PR and #1999, says a reload after a refused open shows an empty assistant. That's only true when nothing was on screen before the open. If an earlier conversation was showing, its id is still in localStorage (it's only cleared when it matches the failed id), so a reload restores that conversation. Either change the code to match, or reword step 3 in both places.
  3. Optional: no test covers the "auth loading" and "login not configured" parts of isSignedOut (hook.ts:159). Dropping !isAuthLoading alone, or isConfigured && alone, leaves every test passing. Consider a test with auth configured and still loading, where a restore returns saved: true, asserting isSaved stays true.
  4. Optional: no test shows the routerRef update is needed (hook.ts:151-154). Deleting routerRef.current = router leaves the URL tests passing, because the mocked router's query never changes after mount. Consider having the mocked replace update the query, or adding a second parameter after mount, and asserting that the strip keeps it.

logan,
messages,
savedAnalysisId,
savedAnalysisId: isSignedOut ? null : savedAnalysisId,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required: by the same reasoning the PR gives for lastSave, nothing reads savedAnalysisId while signed out. Its only reader is the History list, which shows the sign-in prompt instead of itself (chatHistorySelector.tsx:32). So this gate, its doc line in types.ts, and the test "signing out leaves no saved analysis showing as open" (useAssistantChat.autosave.test.ts:554) protect a value nobody sees. It also leaves lastSave ungated and savedAnalysisId gated, which is inconsistent. Consider dropping the gate, the doc line and that test, and updating #1999's first done item to match, since #1999 asked for this. The isSaved gate one line up is needed: ChatPanel shows that label whether or not the user is signed in.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Ready for Review

Development

Successfully merging this pull request may close these issues.

Assistant: clear saved state on sign-out, and strip ?sessionId= after a failed open

3 participants